Repository navigation
fix: motion planning detours & hysteresis, custom referee rules, and benchmark metrics - #147
Merged
Merged
Conversation
…e detours - FastPathPlanner (C++ and Python): key obstacle detour memory on obstacle ID rather than shifting contact points, eliminating dithering oscillations. - TrajectorySamplingPlanner: add temporal speed-modulation candidates (0.75x, 0.55x) and Sumatra-style angular deviation penalty, cutting crossing detour from 1.55x (5.10s) to 1.09x (3.43s) with zero brake events. - TrajectorySamplingPlanner: constrain candidate targets to forward/lateral hemisphere (<= 90 deg), eliminating the 180-deg backward candidate that caused robots to reverse away from dynamic pinches (pinching_contest 4.68s -> 2.57s). - Update visual comparisons and motion planning documentation.
- Add excess_distance_m (path length minus direct Euclidean distance) and near_miss_ratio (% of moving ticks where clearance < 0.15m) to BenchmarkManager. - Record both metrics in results JSON, aggregate summaries, and Markdown report. - Include excess distance (+X.XXm) in terminal progress printout.
… and crashing rule - actions.py: clamp restart positions outside 0.80m keep-out arc (fixing 0.79m floating-point precision boundary penalty). - actions.py: place defending penalty goalkeeper +ROBOT_RADIUS offset on defense line and attacking keeper on own goal line. - crashing_rule.py: do not penalize separating robots when relative crash speed <= threshold. - double_touch_rule.py: correctly attribute double-touch fouls to the last kicker. - Add regression tests in test_custom_referee, test_pushing_crashing, and test_referee_unit.
…and tactics - pickers.py: add margin-based hysteresis to friendly_closer_to_ball to prevent rapid 60Hz role-swapping when robots are equidistant to the ball. - ClearBallTactic: expand activation thresholds (friendly distance 2.6m -> 3.0m, enemy distance 1.2m -> 1.5m) to prevent boundary flickering. - PressAndContainTactic: widen enemy distance threshold (1.4m -> 1.7m) and use Sticky[int] hysteresis for presser robot selection. - DecoyOverloadTactic: add stall/deadlock timeout when no friendly targets are reachable. - Engine/tactics: store all hysteresis state on tactic instances with reset_applicable() and module-level reset hooks for clean test isolation. - Add comprehensive regression tests in test_strategy, test_friendly_closer_to_ball, test_clear_ball_tactic, test_press_and_contain_tactic, and test_decoy_and_overload_tactic.
isaac0804
added a commit
that referenced
this pull request
Oct 26, 2026
…ashboard for the field (#148) * docs: refresh the roadmap against the code, and the stale notes it points at The roadmap still described the pre-evaluate.py evaluation plan, kept a mirror_swap xfail that no longer exists, and listed several items that had already shipped. Rewritten along its own convention (a resolved item becomes a one-line pointer under "Done"), with the remaining items numbered in sequence: - the strategy-evaluation items -> Done: evaluate.py and pool_v1 replaced the situation bench and ladder.py. The two genuinely open parts moved instead of disappearing: the CI stall gate into the stalls entry, the tactic contracts into the ball-holding entry. - directional tube and Replay overlays -> Done; fpp mirror_swap -> Done: the 0.52 m was the defense-area keep-out clamp, not a local planner minimum, and the xfail went in 5f97199. - stalls -> the 2026-10-17 full round-robin figures; sim realism -> Phases 0-2 built, with what remains named. - bang-bang, trajsample, testing gaps and convergence revised for what has landed since; the unrecorded Sumatra findings (#4-7) and the shared-Sticky item removed (Sticky exists and is used). Also fixes the stale notes that pointed into the roadmap: obstacles.py's class docstring still described a growing circle, FORK_NOTES referenced two deleted roadmap sections, sim_realism's status line read as if nothing had been built, and testing_gaps #6 now records the one sim case for ball_placement_interference. * docs: fix the roadmap refresh's errors; cite roadmap entries by title Corrections to the previous commit, checked against the code: - stalls: 1731e90 fixed only the kick-off that waited the 15 s cap; the three COMMITTED_FROZEN stalls of the 2026-10-17 round-robin are not yet diagnosed (docs/strategies.md says the same). - sim realism: P1's value model is fitted (passes 1-3) but not yet a better ranker than goals; only P2 is not started. - Shared hysteresis helper: Sticky's users are PassAndShoot, ClearBall and PressAndContain; pickers.friendly_closer_to_ball keeps its own hysteresis. - grsim entry: drop the note that FORK_NOTES points at a missing section (the previous commit fixed it); "like 10c" names its entry; pool_v1 holds the v3 ranges, v3_seed2 is the round-robin's seeded run of them. The roadmap now asks to cite entries by title, since open items renumber. Every comment, docstring and doc that cited a roadmap number used numbers long gone ("roadmap item 15/16", "item 5", "item 1"); each now names its entry. fastpathplanner_corner_test's docstring said seeds were pinned as stuck; the list has been empty since #147. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(engine): skip the pinned goalkeeper when its robot is not on the field A team with no robot of goalkeeper_id (default 0) crashed on its first tick with KeyError in the goalkeeper tactic. Real tests often run without robot 0. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(tactics): hysteresis on the shot aim in LeadAndSupport and GiveAndGo Against a centred keeper the two corners are near-equal, and the aim flipped between them every tick; a lone carrier turned back and forth ~11 s before shooting. Uses find_best_shot's switch margin, as _pass_and_score does. Changes strategy code: cached matches rerun. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(examples): one attacker vs one defender on real robots or in sim Field, camera half, vision/firmware IDs, IR trust and tactics in one config block; operator-only referee driven from the dashboard; starts in HALT. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(engine): no goalkeeper row in debug_status when its robot is absent A team without the goalkeeper's robot showed a phantom goalkeeper (e.g. ID 15) in the dashboard's tactics list. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(run): field check on camera runs, auto-HALT on real robots FieldCheck judges raw vision, the geometry packet and radio feedback: each driven robot seen, under its ID, inside field_bounds; camera sending; field size; radio still answering; unconfigured robots in play. The Live tab shows the rows, dims everything outside field_bounds and draws the field as ssl-vision sees it. On real robots any failing row forces HALT. The dashboard opens on Live when a match is attached; Space only halts. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(run): robot profiles: push without a kicker, gentle carry without a dribbler RobotProfile(kicker, dribbler, ir) per robot, passed to StrategyRunner as {yellow,blue}_robot_profiles and applied in every mode, so a test with a broken robot can be rehearsed in a simulator. ProfiledRobotController wraps the team's robot controller: a kick becomes a 0.3 s forward push at 1 m/s (a stop command ends it); a dribble request without a dribbler never backs up and is scaled to 0.5 m/s and 2 rad/s together. A robot without a ball sensor is left out of the trusted-IR set. A profile for a robot that is not driven, or ball sensors given both ways, stops the run at startup. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(examples,dashboard): per-robot hardware and RULES switch; tidier Live page The attacker/defender example takes one TestRobot per robot (vision ID, firmware ID, RobotProfile) and RULES = "off" | "on" ("on": the human referee profile, nothing auto-advances). The Live page lists only failing and warning field-check rows, folds the passing ones into an "N ok" line, hides the restart buttons when no rule sets up a restart, and drops the robot list the field check already covers. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(run,analysis): record field tests as SSL game logs; measure robots from them StrategyRunner(game_log_path=...) records a camera run in the league's game-log format: every vision packet as received, the referee's state, the radio's bytes both ways and a run-info record (ID mappings, profiles), all on this machine's clock. tools/convert_ssl_log.py reads it unchanged. The attacker/defender example records every camera run to replays/field/, and --calibrate plays fixed moves (ScriptedMovesTactic: rest, steps at 0.3/0.6/1 m/s, sideways, turns; a pause restarts the interrupted move). tools/robot_calibration.py measures each robot from such a log: loop delay, speed gain and acceleration per axis (the sim's move model fitted to the camera's positions), vision noise at rest, missed detections and kick speed. On a synthetic robot it recovers delay within 10 ms, acceleration within 15 % and gain within 1 %. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): match timeline in the Replay and Live tabs; 15x smaller replay payload timeline.js draws tools/match_timeline.py's swimlanes in the browser: the referee command band, one lane per friendly robot coloured by its tactic with a strip where it had the ball, each robot's distance to the ball, and the event markers. In the Replay tab it follows the cursor and seeks on click or drag (30 s / 60 s / 3 min / whole match); its tactic spans match match_timeline.tactic_changes exactly on a tournament match. In the Live tab each streamed state extends the lanes, stamped with the frame's game time (now sent with the state), keeping the last 5 minutes. A 13-minute replay's frames were 108 MB of JSON (full 17-digit floats) against a 16 MB .npz. Positions now go to the millimetre, velocities to the centimetre, and responses over 64 KB are gzipped: 7.4 MB. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): Play list of situations; possession band, switch ticks, shots The Replay side panel's raw event and intention logs were hard to read. A Play list replaces them: one row per situation (a restart going live or a side winning the ball, from goal_ledger), with its third, length, shots and how it ended (the stoppage's reason, or who won the ball and how). Click a row to seek; the raw intention log stays, folded. The timeline gains a possession band, tactic switch ticks, shots and hover tooltips. The slow analysis (ball losses, shots) is kept beside the replay in <stem>.timeline.json, and npz frames are built straight from the columns, so a replay serves in ~3 s instead of ~16 s. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(refiners): an unseen ball rolls to a stop inside vision's area Unseen, the ball filter carried the ball on at its last velocity, and the velocity refiner kept that velocity alive from the guessed positions: a ball hidden by a robot drifted away, and one that left vision's area flew off for good (an rsim attacker/defender run had it 600 m out after 15 minutes). Now it rolls on from where it was lost, slowing at BALL_ROLL_DECEL (0.29 m/s^2, official Div B logs) until it stops, and never leaves the area vision accepts. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * docs(examples): a test day's measurements: calibration twice, floor friction, kicks, occlusions Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(examples): run as documented, `pixi run python examples/<name>.py` utama_core is not installed in the pixi environment, so every example failed at its first import when run by path, as their docstrings say to (the field-test example included). Each now puts the repo root on sys.path first, as the scripts in tools/ do. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): ball placement by right-click; phone layout; lighter Live page - Right-click (long-press on a phone) on the Live field: ball placement to that spot by Yellow or Blue, as the old referee GUI's God Mode had. In a simulator the ball is moved there. - Below 760 px wide the panels stack, the page scrolls and buttons are bigger. - The field shows the 0.3 m run-off; a ball beyond the view is drawn hollow at its edge. - States arrive at 60 Hz: the field and timeline draw once per screen frame, the side panel's text 4 times a second (it was rebuilt on every state, so a click could miss). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * perf(dashboard): send a replay's ticks as columns of whole numbers One JSON object per tick, every key repeated, made a match 65 MB (7.4 MB gzipped) and took 4 s to build. Columns of mm, cm/s and mrad with `seen` masks are 21 MB (4.3 MB gzipped) and build in 1.5 s; the page rebuilds the same per-tick frames in about 0.2 s, so nothing else changes. Pickled replays keep the old shape. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a client that stops reading no longer freezes the control loop `notify` writes each state to every stream client on the control loop (`CustomReferee.step`), on blocking sockets with no timeout. A client that stopped reading (a phone locked with the page open, a laptop asleep, WiFi gone) filled its buffers and blocked the loop for good: no radio commands, no HALT, no auto-HALT. Stream sockets are now non-blocking: once a client's buffers are full its write fails at once, it is dropped and its browser reconnects. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(runner): an auto-HALT is the command of the tick that forced it The field check ran after the referee stepped, so the tick that forced HALT still carried the command it replaced: pressing Force Start (or a placement) while a check failed sent one tick of motion. Vision is now taken and judged before the referee steps. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): the auto-HALT banner clears once play resumes; HALT is drawn first The runner kept the last auto-HALT's reason for good, so the red banner stayed up with the robots playing, and the Live page, which skipped its 4 Hz panel throttle while a reason was set, rebuilt the side panel on every state from then on. The reason now lasts as long as the HALT it forced. The command and the banner are drawn first on every frame, before the field and timeline, so neither waits on the throttle or fails with a drawing error. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a columnar replay's last Play row ends at its last tick The end was read from `frames`, which a columnar replay leaves empty since its ticks are sent as columns, so the last row ended at 0 s and read "-29 s" with no shots. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a robot's heading is mirrored with the view With our goal on the right in a view that mirrors x (rsim, replays), the heading dash was drawn from the raw orientation, so a robot facing +x pointed the wrong way. It now goes through the same field-to-screen transform as positions. The test runs field_canvas.js in node with a 2D context that records lines, skipped where node is missing. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a dropped stream client is closed, so its browser reconnects d9e6f2e dropped a client that stopped reading from the list but left its connection open: its keep-alives kept it alive, so a phone woken up got keep-alives and no states, its page frozen with the dot green, and a failed write could leave half a frame in the stream. A client whose write fails is now shut down and its browser reconnects (after 0.5 s: the stream sets `retry`). States and keep-alives, written from two threads, share a lock per client so neither cuts into the other's frame. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a ball placement must be inside the field and the area in play Any point on the Live page was accepted, the run-off and the half the camera does not see included, so the placing robots could drive out of vision. The command handler now refuses a target outside the field lines or, with a camera, outside the field check's area in play: a 400 with the reason, and nothing reaches the referee. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): Live shows refused commands and a stalled stream; Halt pinned on a phone - A command the server refused (a placement outside the area in play) or never got shows under the buttons for 6 s; it was only logged to the browser console, so a Halt that did not go through looked the same as one that did. - No state for half a second turns the dot grey and shows "no update for N s" beside the command: the page was showing the past with the dot green. - On a phone, Halt floats in the bottom-right corner, in reach wherever the page is scrolled; it sat below the field, the timeline and the field check. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): a held touch opens the ball-placement menu (iPhone) iPhone Safari fires no contextmenu on a long press, so the menu never opened there. A touch held still for 0.6 s opens it too; a click within 0.4 s of opening (the lifted finger) no longer closes it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(refiners): with smoothing off, a second ball loss starts where the ball was last seen Smoothing off and imputation on, the ball filter runs only while the ball is unseen, so it never heard the ball had been seen again: a second loss carried on the first one's roll from where that one started. A seen ball now resets the filter when it is not run. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
isaac0804
added a commit
that referenced
this pull request
Oct 27, 2026
…al-robot examples, in-house vision plan (#149) * docs: refresh the roadmap against the code, and the stale notes it points at The roadmap still described the pre-evaluate.py evaluation plan, kept a mirror_swap xfail that no longer exists, and listed several items that had already shipped. Rewritten along its own convention (a resolved item becomes a one-line pointer under "Done"), with the remaining items numbered in sequence: - the strategy-evaluation items -> Done: evaluate.py and pool_v1 replaced the situation bench and ladder.py. The two genuinely open parts moved instead of disappearing: the CI stall gate into the stalls entry, the tactic contracts into the ball-holding entry. - directional tube and Replay overlays -> Done; fpp mirror_swap -> Done: the 0.52 m was the defense-area keep-out clamp, not a local planner minimum, and the xfail went in 5f97199. - stalls -> the 2026-10-17 full round-robin figures; sim realism -> Phases 0-2 built, with what remains named. - bang-bang, trajsample, testing gaps and convergence revised for what has landed since; the unrecorded Sumatra findings (#4-7) and the shared-Sticky item removed (Sticky exists and is used). Also fixes the stale notes that pointed into the roadmap: obstacles.py's class docstring still described a growing circle, FORK_NOTES referenced two deleted roadmap sections, sim_realism's status line read as if nothing had been built, and testing_gaps #6 now records the one sim case for ball_placement_interference. * docs: fix the roadmap refresh's errors; cite roadmap entries by title Corrections to the previous commit, checked against the code: - stalls: 1731e90 fixed only the kick-off that waited the 15 s cap; the three COMMITTED_FROZEN stalls of the 2026-10-17 round-robin are not yet diagnosed (docs/strategies.md says the same). - sim realism: P1's value model is fitted (passes 1-3) but not yet a better ranker than goals; only P2 is not started. - Shared hysteresis helper: Sticky's users are PassAndShoot, ClearBall and PressAndContain; pickers.friendly_closer_to_ball keeps its own hysteresis. - grsim entry: drop the note that FORK_NOTES points at a missing section (the previous commit fixed it); "like 10c" names its entry; pool_v1 holds the v3 ranges, v3_seed2 is the round-robin's seeded run of them. The roadmap now asks to cite entries by title, since open items renumber. Every comment, docstring and doc that cited a roadmap number used numbers long gone ("roadmap item 15/16", "item 5", "item 1"); each now names its entry. fastpathplanner_corner_test's docstring said seeds were pinned as stuck; the list has been empty since #147. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(engine): skip the pinned goalkeeper when its robot is not on the field A team with no robot of goalkeeper_id (default 0) crashed on its first tick with KeyError in the goalkeeper tactic. Real tests often run without robot 0. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(tactics): hysteresis on the shot aim in LeadAndSupport and GiveAndGo Against a centred keeper the two corners are near-equal, and the aim flipped between them every tick; a lone carrier turned back and forth ~11 s before shooting. Uses find_best_shot's switch margin, as _pass_and_score does. Changes strategy code: cached matches rerun. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(examples): one attacker vs one defender on real robots or in sim Field, camera half, vision/firmware IDs, IR trust and tactics in one config block; operator-only referee driven from the dashboard; starts in HALT. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(engine): no goalkeeper row in debug_status when its robot is absent A team without the goalkeeper's robot showed a phantom goalkeeper (e.g. ID 15) in the dashboard's tactics list. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(run): field check on camera runs, auto-HALT on real robots FieldCheck judges raw vision, the geometry packet and radio feedback: each driven robot seen, under its ID, inside field_bounds; camera sending; field size; radio still answering; unconfigured robots in play. The Live tab shows the rows, dims everything outside field_bounds and draws the field as ssl-vision sees it. On real robots any failing row forces HALT. The dashboard opens on Live when a match is attached; Space only halts. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(run): robot profiles: push without a kicker, gentle carry without a dribbler RobotProfile(kicker, dribbler, ir) per robot, passed to StrategyRunner as {yellow,blue}_robot_profiles and applied in every mode, so a test with a broken robot can be rehearsed in a simulator. ProfiledRobotController wraps the team's robot controller: a kick becomes a 0.3 s forward push at 1 m/s (a stop command ends it); a dribble request without a dribbler never backs up and is scaled to 0.5 m/s and 2 rad/s together. A robot without a ball sensor is left out of the trusted-IR set. A profile for a robot that is not driven, or ball sensors given both ways, stops the run at startup. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(examples,dashboard): per-robot hardware and RULES switch; tidier Live page The attacker/defender example takes one TestRobot per robot (vision ID, firmware ID, RobotProfile) and RULES = "off" | "on" ("on": the human referee profile, nothing auto-advances). The Live page lists only failing and warning field-check rows, folds the passing ones into an "N ok" line, hides the restart buttons when no rule sets up a restart, and drops the robot list the field check already covers. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(run,analysis): record field tests as SSL game logs; measure robots from them StrategyRunner(game_log_path=...) records a camera run in the league's game-log format: every vision packet as received, the referee's state, the radio's bytes both ways and a run-info record (ID mappings, profiles), all on this machine's clock. tools/convert_ssl_log.py reads it unchanged. The attacker/defender example records every camera run to replays/field/, and --calibrate plays fixed moves (ScriptedMovesTactic: rest, steps at 0.3/0.6/1 m/s, sideways, turns; a pause restarts the interrupted move). tools/robot_calibration.py measures each robot from such a log: loop delay, speed gain and acceleration per axis (the sim's move model fitted to the camera's positions), vision noise at rest, missed detections and kick speed. On a synthetic robot it recovers delay within 10 ms, acceleration within 15 % and gain within 1 %. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): match timeline in the Replay and Live tabs; 15x smaller replay payload timeline.js draws tools/match_timeline.py's swimlanes in the browser: the referee command band, one lane per friendly robot coloured by its tactic with a strip where it had the ball, each robot's distance to the ball, and the event markers. In the Replay tab it follows the cursor and seeks on click or drag (30 s / 60 s / 3 min / whole match); its tactic spans match match_timeline.tactic_changes exactly on a tournament match. In the Live tab each streamed state extends the lanes, stamped with the frame's game time (now sent with the state), keeping the last 5 minutes. A 13-minute replay's frames were 108 MB of JSON (full 17-digit floats) against a 16 MB .npz. Positions now go to the millimetre, velocities to the centimetre, and responses over 64 KB are gzipped: 7.4 MB. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): Play list of situations; possession band, switch ticks, shots The Replay side panel's raw event and intention logs were hard to read. A Play list replaces them: one row per situation (a restart going live or a side winning the ball, from goal_ledger), with its third, length, shots and how it ended (the stoppage's reason, or who won the ball and how). Click a row to seek; the raw intention log stays, folded. The timeline gains a possession band, tactic switch ticks, shots and hover tooltips. The slow analysis (ball losses, shots) is kept beside the replay in <stem>.timeline.json, and npz frames are built straight from the columns, so a replay serves in ~3 s instead of ~16 s. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(refiners): an unseen ball rolls to a stop inside vision's area Unseen, the ball filter carried the ball on at its last velocity, and the velocity refiner kept that velocity alive from the guessed positions: a ball hidden by a robot drifted away, and one that left vision's area flew off for good (an rsim attacker/defender run had it 600 m out after 15 minutes). Now it rolls on from where it was lost, slowing at BALL_ROLL_DECEL (0.29 m/s^2, official Div B logs) until it stops, and never leaves the area vision accepts. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * docs(examples): a test day's measurements: calibration twice, floor friction, kicks, occlusions Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(examples): run as documented, `pixi run python examples/<name>.py` utama_core is not installed in the pixi environment, so every example failed at its first import when run by path, as their docstrings say to (the field-test example included). Each now puts the repo root on sys.path first, as the scripts in tools/ do. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): ball placement by right-click; phone layout; lighter Live page - Right-click (long-press on a phone) on the Live field: ball placement to that spot by Yellow or Blue, as the old referee GUI's God Mode had. In a simulator the ball is moved there. - Below 760 px wide the panels stack, the page scrolls and buttons are bigger. - The field shows the 0.3 m run-off; a ball beyond the view is drawn hollow at its edge. - States arrive at 60 Hz: the field and timeline draw once per screen frame, the side panel's text 4 times a second (it was rebuilt on every state, so a click could miss). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * perf(dashboard): send a replay's ticks as columns of whole numbers One JSON object per tick, every key repeated, made a match 65 MB (7.4 MB gzipped) and took 4 s to build. Columns of mm, cm/s and mrad with `seen` masks are 21 MB (4.3 MB gzipped) and build in 1.5 s; the page rebuilds the same per-tick frames in about 0.2 s, so nothing else changes. Pickled replays keep the old shape. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a client that stops reading no longer freezes the control loop `notify` writes each state to every stream client on the control loop (`CustomReferee.step`), on blocking sockets with no timeout. A client that stopped reading (a phone locked with the page open, a laptop asleep, WiFi gone) filled its buffers and blocked the loop for good: no radio commands, no HALT, no auto-HALT. Stream sockets are now non-blocking: once a client's buffers are full its write fails at once, it is dropped and its browser reconnects. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(runner): an auto-HALT is the command of the tick that forced it The field check ran after the referee stepped, so the tick that forced HALT still carried the command it replaced: pressing Force Start (or a placement) while a check failed sent one tick of motion. Vision is now taken and judged before the referee steps. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): the auto-HALT banner clears once play resumes; HALT is drawn first The runner kept the last auto-HALT's reason for good, so the red banner stayed up with the robots playing, and the Live page, which skipped its 4 Hz panel throttle while a reason was set, rebuilt the side panel on every state from then on. The reason now lasts as long as the HALT it forced. The command and the banner are drawn first on every frame, before the field and timeline, so neither waits on the throttle or fails with a drawing error. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a columnar replay's last Play row ends at its last tick The end was read from `frames`, which a columnar replay leaves empty since its ticks are sent as columns, so the last row ended at 0 s and read "-29 s" with no shots. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a robot's heading is mirrored with the view With our goal on the right in a view that mirrors x (rsim, replays), the heading dash was drawn from the raw orientation, so a robot facing +x pointed the wrong way. It now goes through the same field-to-screen transform as positions. The test runs field_canvas.js in node with a 2D context that records lines, skipped where node is missing. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a dropped stream client is closed, so its browser reconnects d9e6f2e dropped a client that stopped reading from the list but left its connection open: its keep-alives kept it alive, so a phone woken up got keep-alives and no states, its page frozen with the dot green, and a failed write could leave half a frame in the stream. A client whose write fails is now shut down and its browser reconnects (after 0.5 s: the stream sets `retry`). States and keep-alives, written from two threads, share a lock per client so neither cuts into the other's frame. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a ball placement must be inside the field and the area in play Any point on the Live page was accepted, the run-off and the half the camera does not see included, so the placing robots could drive out of vision. The command handler now refuses a target outside the field lines or, with a camera, outside the field check's area in play: a 400 with the reason, and nothing reaches the referee. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): Live shows refused commands and a stalled stream; Halt pinned on a phone - A command the server refused (a placement outside the area in play) or never got shows under the buttons for 6 s; it was only logged to the browser console, so a Halt that did not go through looked the same as one that did. - No state for half a second turns the dot grey and shows "no update for N s" beside the command: the page was showing the past with the dot green. - On a phone, Halt floats in the bottom-right corner, in reach wherever the page is scrolled; it sat below the field, the timeline and the field check. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): a held touch opens the ball-placement menu (iPhone) iPhone Safari fires no contextmenu on a long press, so the menu never opened there. A touch held still for 0.6 s opens it too; a click within 0.4 s of opening (the lifted finger) no longer closes it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(refiners): with smoothing off, a second ball loss starts where the ball was last seen Smoothing off and imputation on, the ball filter runs only while the ball is unseen, so it never heard the ball had been seen again: a second loss carried on the first one's roll from where that one started. A seen ball now resets the filter when it is not run. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(engine): a tactic's overlay is cleared once it stops being traced trace_if_changed only logs a change, so a key a tactic stopped tracing (the tactic stopped running, or its phase ended) kept its last value and the replay kept drawing it. Strategy.tick now records None for every key not traced that tick. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): bigger robot labels, and a role label no longer covers the robot number Labels scale with the robot (10 px at least) and have a halo so they read over lines; a tactic's highlight label goes under the robot. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): the replay shows the referee's match clock; a key for the referee band The replay time keeps running through stoppages, so a 10 min match reads 12-13 min; the match clock beside it runs only in play, as the referee's does. The legend now names the referee band's colours (a short STOP or placement is too narrow to carry its name), and the ball row says how a situation started ("from kickoff") rather than the restart's name. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(dashboard): a command from the Live tab is stamped on the referee's clock The page's commands were stamped with the wall clock, but in rsim the referee runs on sim time: one button press put its match clock (and the STOP and free-kick timers) 1.8e9 s out, shown as "29878093:22 left". They now take the time of the referee's last step (CustomReferee.now). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(dashboard): choose or restart each side's strategy from the Live tab StrategyRunner.replace_strategy runs a new strategy for a side from the next tick, only while halted; vision, the radio and the dashboard carry on. The script lists the choices (dashboard/views/strategy.py) and the Live tab shows a list and Run per side; Run on the one running restarts it. The field example offers its attacker and defender tactics. The example's robot now holds the shot line while its tactic does not apply: press applies only with an opponent near the ball, and picking it anyway made the kernel raise and stop the run. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * docs: the Live tab's Strategy panel Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(run,dashboard): an operator's HALT lasts in the simulators; a taken dashboard port fails at start In rsim and grSim the runner resumes a HALT after 5 s, for tournaments with nobody to end a HALT the rules call. It resumed the operator's Halt too, and the field example's starting HALT, so a grSim test was playing before anyone pressed Start and the Strategy panel had 5 s to act. A HALT called from the dashboard (or a run started halted with the dashboard attached) now lasts until the operator ends it; the rules' HALTs still resume. A dashboard whose port was taken failed in its server thread and the match ran on without one, the open page showing the other dashboard. It now raises when started. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(run): on camera runs our robots stop at the edge of the area in play A tactic chasing a ball out of the camera's half drove its robot after it (0.9 m past the half-way line in a grSim test of real_attacker_defender), out of sight of vision. The planners' walls at field_bounds are obstacles to plan around, not a limit, and the field check only halts once a robot is 0.3 m out. FencedRobotController sits under the strategy in grSim and real mode and caps each robot's speed towards each edge to what it can stop from before the line (2 m/s^2, 0.1 s delay); speed away from an edge is never limited, so a robot outside drives back in. In grSim, a ball rolled out repeatedly for 25 s: 0.52 m outside without it, 0.001 m with. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * docs,tools: the real-robot test-day procedure; a vision check docs/real_robot_setup.md: ssl-vision on its own laptop, WSL mirrored networking and the Hyper-V firewall rule for vision, the radio through usbipd by hardware ID with auto-attach, teleop, and the field check, with the failures met on the first setup (2026-10-22) and their causes. It replaces setup_external.md's short WSL section. tools/vision_check.py says whether ssl-vision's multicast reaches this machine, per camera, with no radio or runner. tools/ssl-vision-lab-field.patch adds our field as a preset to ssl-vision. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * examples(real_attacker_defender): the lab field, 3 m x 2.25 m Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * tools(vision_check): each robot and ball where each camera puts it, and the gap Two cameras over the whole field disagreed on robot positions (graphicalClient showed them jumping between two spots); the gap in mm shows whether a calibration change helped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(vision): weight each camera by how steeply it sees a robot; drop one that disagrees Two cameras over the whole lab field (2026-10-22), each behind a goal, put a robot 17 cm apart at the far end; CameraCombiner averaged them, so the robot sat 9 cm off both and jumped 9 cm whenever a camera missed a frame. The receiver now keeps each camera's position from ssl-vision's geometry calibrations and stamps it on that camera's frames; the combiner weights each detection by cos^4 of its camera's view angle and drops one further than CAMERA_AGREEMENT (0.1 m) from the best-placed camera's. Without positions (rsim, grSim, an uncalibrated camera) every camera counts the same, as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * examples: real_score_goal, one robot alone takes the ball and scores For a test day with a single robot ready. Field and camera view come from real_attacker_defender.py so the two agree; starts in HALT with the field check and dashboard as there. Scores in rsim within 5 s of Force Start. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(run): a team alone in real mode tracks only its own mapped robots, whatever its colour The roster allowlists assumed our team was yellow: with our blue robot alone (real_score_goal.py, 2026-10-22) blue went unfiltered and yellow was filtered, so misread patterns (blue 1, blue 5 beside our blue 2) became our robots and lived on in the Kalman filter as ghosts. A colour we drive is now filtered to its mapping's IDs; the opponent's colour without an opponent strategy stays unfiltered. The field check still reads raw vision, so it keeps listing such detections as 'not configured'. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * examples: real_dribble_and_shoot; fix(dribble): letting go backs away instead of waiting DribbleTactic stood still after releasing the ball until it was 13 cm away; a ball that stopped at 11 cm froze the robot for good (rsim, 2026-10-22). It now backs off at 0.2 m/s until the ball is clear. real_dribble_and_shoot: one robot dribbles a lap, then LeadAndSupportTactic shoots; real_score_goal's build_runner takes the tactics, offered in the Strategy panel. In rsim the robot still stalls on the second segment with the ball (the planner's command shrinks to ~0, DribbleTactic's known rsim issue); untested on a real robot. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(vision): fuse the ball across cameras like the robots, and keep a one-camera ball at its robot The robot was averaged across cameras while each camera's ball stayed separate (merged only within 5 cm) and one was picked, so with the cameras ~6 cm apart the ball at the dribbler came out inside the robot. Ball detections within BALL_MERGE_THRESHOLD (now 0.2 m) are fused by the robots' rule, and a ball one camera alone sees (the robot hides it from the other) is moved by that camera's offset at the nearest robot it also sees (BALL_ROBOT_SHIFT_RANGE). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * tools(vision_check): --guided checks each camera against 9 known spots and saves the baseline Cameras that agree with each other can both be wrong. The ball (or a robot, facing +x) goes on the corners, edge midpoints and centre of the announced field; each camera's error is printed per spot and as mean, worst and shift, and saved to replays/field/. A stray ball detection far from the spot (camera 0 sees one 824 mm away) is left out. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * docs: plan an in-house vision stack for lab days, with the experiments that decide it The 2026-10-22 test day's vision findings, the options considered, the design (per-session tag calibration, tags on robots, ball inside the field mask, SSL packets out), two experiments with pass marks, and the order of work. Linked from the roadmap and the docs index. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(run): the fence holds only edges inside the field, and fences a kicker-less push With the cameras on the whole field (and in grSim by default) the fence was the touch and goal lines: a robot could not cross them to fetch a ball out of play, so a free kick or placement there never completed, and the keeper could not step into its goal mouth. Only edges inside the field (the half-way line on a half-field run) are fenced now; none when the cameras see it all. The fence also wrapped the profile layer, which turns a kicker-less robot's kick into a 0.3 m push the fence never saw. The profile is outside it now. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(vision_check): --guided measures each spot from packets after Enter Packets queued while the operator carried the robot to the next spot were read first, so the previous spot (1.1-1.5 m away) and the carry were averaged into the spot's error and the saved baseline. The socket is drained on Enter. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(vision): a camera at height 0 counts as one whose position is unknown A camera not calibrated yet reports height 0, which gave every robot it sees a view-angle weight of 0: a robot only it saw divided by zero in CameraCombiner. Also corrects the comment: grSim sends camera positions too, so the weighting is active there. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(vision): a camera that loses the robot for a frame keeps the ball at its dribbler When camera 1 kept the ball at the dribbler but lost the robot for a frame or two, the robot came from camera 0 alone, 8.5 cm from camera 1's view, and the ball was not shifted: it sat inside the robot in 95 of 3,971 frames of 20261022_211142_real_score_goal.log. CameraCombiner now remembers each camera's gap to the others at each robot (BALL_ROBOT_OFFSET_MEMORY, 0.5 s), and places a robot a camera has just lost at another camera's detection plus that gap. The refiner keeps one combiner across frames. Replayed, that log has no ball inside the robot; the nearest is 94 mm, as in camera 1's own view. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(refiners): the ball is one in the field or a goal, nearest the last, not the most confident 2026-10-22: camera 0 saw up to four steady "balls" 15 cm past the +y touch line, as confident as the real one. The most confident ball, ties going to list order, was a fake 22% of the time, and the ball flickered between them. PositionRefiner now takes a ball inside the field lines or a goal when there is one, then the one nearest the last ball; with no last ball, the most confident. Replayed over the day's logs, no log has more ball jumps of over 0.3 m in one frame; in 20261022_213347_real_score_goal.log they fall from 5,439 to 4, and frames with the ball off the field from 7,317 to 88. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * docs(sim_realism): first measurements of our robot from the 2026-10-22 field logs Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * docs(in_house_vision): the team chose the in-house stack; plan in order with pass marks Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * docs(in_house_vision): how calibration tells floor and raised tags apart, a session, the code Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * feat(game_log): record which code ran, operator and strategy-change notes, and the world beside the log RUN_INFO now holds the command line, the git commit and whether the tree was dirty, and the field bounds. A NOTE record (103) carries notes typed in the Live tab and the runner's own strategy changes (and dropped ones). A run with a game log also writes the filtered world as a columnar replay beside it, saved once at close: a checkpoint took 385 ms ten minutes in. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(strategy_view): show running only what the runner swapped in; queue a strategy with its name The Strategy panel showed a change as running as soon as it was queued, so one dropped because play restarted first still showed. It now shows StrategyRunner.strategy_names, set at the swap. The dashboard thread queued the strategy and its name in two writes; a swap between them noted the class name and left the chosen name to label the next change. One entry now holds both. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5 * fix(vision): preserve plausible nearby ball observations (#150) * fix(vision): preserve plausible nearby ball observations Prefer plausible nearby alternatives before per-camera confidence selection, and keep each camera from overweighting close duplicate observations. Preserve observed ball/robot geometry when independent smoothing would move the ball inside its robot, while retaining the existing estimate during camera loss. Add proximity-boundary, confidence, field/goal, camera-loss and minimized real-log regression tests. * fix(vision): retain valid boundary observations and reset dimensions Filter original robot detections by vision bounds before averaging same-camera repeats, and retain field dimensions when resetting the camera combiner. Add 28 regression cases covering both teams, all four vision boundaries, position bias, and repeated resets with smoothing enabled and disabled. All 168 refiner tests pass; formatting and lint checks pass. * fix(ci): separate push and pull request concurrency groups Include the event type in each branch's concurrency group so the push and pull-request checks cannot cancel each other on the same commit. Continue canceling superseded runs for each event and branch. * fix(vision): keep single-camera ticks in fused frame instead of post-Kalman clamp --------- Co-authored-by: isaac0804 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Fred Huang <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Comprehensive audit and stabilization across motion planning, referee overrides, and tactic partitioning:
1. Motion Planning
crossing, completion time dropped from 5.10s -> 3.43s (-1.67s), path ratio dropped from 1.55x -> 1.09x, and brake events dropped from 3.0 -> 0.0.pinching_contesttime dropped from 4.68s -> 2.57s and path ratio from 1.26x -> 1.06x).excess_distance_mandnear_miss_ratio(% of moving ticks where clearance < 0.15m) across results JSON, markdown tables, and terminal reporting.2. Custom Referee Rules & Actions
3. Strategy & Tactics Hysteresis
friendly_closer_to_ballto prevent rapid 60Hz role-swapping between equidistant robots.ClearBallTactic(2.6m -> 3.0m) andPressAndContainTactic(1.4m -> 1.7m) to prevent boundary flickering; addedSticky[int]hysteresis to presser selection.DecoyOverloadTactic.reset_applicable()and test teardown hooks.Test Verification
pixi run lint: 100% clean (black, ruff, isort).pixi run test: 12,885 passed, 83 xfailed, 0 failed.trajsample.